using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using NFreeMarker.Ext.Beans;
using NFreeMarker.Template;
using NFreeMarker.Template.Utility;

namespace NFreeMarker.Core
{
    /**
     * This is a common superclass of {@link freemarker.template.Configuration},
     * {@link freemarker.template.Template}, and {@link Environment} classes.
     * It provides settings that are common to each of them. FreeMarker
     * uses a three-level setting hierarchy - the return value of every setting
     * getter method on <code>Configurable</code> objects inherits its value from its parent 
     * <code>Configurable</code> object, unless explicitly overridden by a call to a 
     * corresponding setter method on the object itself. The parent of an 
     * <code>Environment</code> object is a <code>Template</code> object, the
     * parent of a <code>Template</code> object is a <code>Configuration</code>
     * object.
     *
     * @version $Id: Configurable.java,v 1.23.2.2 2007/04/02 13:46:43 szegedia Exp $
     * @author Attila Szegedi
     */

    public class Configurable
    {
        public const string LocaleKey = "locale";
        public const string NumberFormatKey = "number_format";
        public const string TimeFormatKey = "time_format";
        public const string DateFormatKey = "date_format";
        public const string DatetimeFormatKey = "datetime_format";
        public const string TimeZoneKey = "time_zone";
        public const string ClassicCompatibleKey = "classic_compatible";
        public const string TemplateExceptionHandlerKey = "template_exception_handler";
        public const string ArithmeticEngineKey = "arithmetic_engine";
        public const string ObjectWrapperKey = "object_wrapper";
        public const string BooleanFormatKey = "boolean_format";
        public const string OutputEncodingKey = "output_encoding";
        public const string UrlEscapingCharsetKey = "url_escaping_charset";
        public const string StrictBeanModels = "strict_bean_models";

        private const char Comma = ',';

        private Configurable _parent;
        private readonly Dictionary<string, string> _properties;
        private readonly Dictionary<object, object> _customAttributes;

        private CultureInfo _locale;
        private string _numberFormat;
        private string _timeFormat;
        private string _dateFormat;
        private string _dateTimeFormat;
        private TimeZoneInfo _timeZone;
        private string _trueFormat;
        private string _falseFormat;
        private bool? _classicCompatible;
        private ITemplateExceptionHandler _templateExceptionHandler;
        private ArithmeticEngine _arithmeticEngine;
        private IObjectWrapper _objectWrapper;
        private Encoding _outputEncoding;
        private bool _outputEncodingSet;
        private Encoding _urlEscapingCharset;
        private bool _urlEscapingCharsetSet;

        public Configurable()
        {
            _parent = null;
            _locale = CultureInfo.CurrentCulture;
            _timeZone = TimeZoneInfo.Local;
            _numberFormat = "number";
            _timeFormat = "";
            _dateFormat = "";
            _dateTimeFormat = "";
            _trueFormat = "true";
            _falseFormat = "false";
            _classicCompatible = false;
            _templateExceptionHandler = TemplateExceptionHandler.DebugHandler;
            _arithmeticEngine = ArithmeticEngine.ConservativeEngine; // TODO: BigDecimalEngine;
            _objectWrapper = ObjectWrapper.DefaultWrapper;
            // outputEncoding and urlEscapingCharset defaults to null,
            // which means "not specified"

            _properties = new Dictionary<string, string>
            {
                {LocaleKey, _locale.Name},
                {TimeFormatKey, _timeFormat},
                {DateFormatKey, _dateFormat},
                {DatetimeFormatKey, _dateTimeFormat},
                {TimeZoneKey, _timeZone.StandardName},
                {NumberFormatKey, _numberFormat},
                {ClassicCompatibleKey, _classicCompatible.ToString()},
                {TemplateExceptionHandlerKey, _templateExceptionHandler.GetType().FullName},
                {ArithmeticEngineKey, _arithmeticEngine.GetType().FullName},
                {BooleanFormatKey, "true,false"}
            };
            // as outputEncoding and urlEscapingCharset defaults to null, 
            // they are not set

            _customAttributes = new Dictionary<object, object>();
        }

        /**
         * Creates a new instance. Normally you do not need to use this constructor,
         * as you don't use <code>Configurable</code> directly, but its subclasses.
         */

        public Configurable(Configurable parent)
        {
            _parent = parent;
            _locale = null;
            _numberFormat = null;
            _trueFormat = null;
            _falseFormat = null;
            _classicCompatible = null;
            _templateExceptionHandler = null;
            _properties = new Dictionary<string, string>(parent._properties);
            _customAttributes = new Dictionary<object, object>();
        }

        //    protected object clone() throws CloneNotSupportedException {
        //        Configurable copy = (Configurable)super.clone();
        //        copy.properties = new Properties(properties);
        //        copy.customAttributes = (HashMap)customAttributes.clone();
        //        return copy;
        //    }

        /**
         * Returns the parent <tt>Configurable</tt> object of this object.
         * The parent stores the default values for this configurable. For example,
         * the parent of the {@link freemarker.template.Template} object is the
         * {@link freemarker.template.Configuration} object, so setting values not
         * specfied on template level are specified by the confuration object.
         *
         * @return the parent <tt>Configurable</tt> object, or null, if this is
         *    the root <tt>Configurable</tt> object.
         */

        public Configurable GetParent()
        {
            return _parent;
        }

        /**
         * Reparenting support. This is used by Environment when it includes a
         * template - the included template becomes the parent configurable during
         * its evaluation.
         */

        internal void SetParent(Configurable parent)
        {
            _parent = parent;
        }

        /**
         * Toggles the "Classic Compatibile" mode. For a comprehensive description
         * of this mode, see {@link #isClassicCompatible()}.
         */

        public void SetClassicCompatible(bool classicCompatibility)
        {
            _classicCompatible = classicCompatibility;
            _properties[ClassicCompatibleKey] = _classicCompatible.ToString();
        }

        /**
         * Returns whether the engine runs in the "Classic Compatibile" mode.
         * When this mode is active, the engine behavior is altered in following
         * way: (these resemble the behavior of the 1.7.x line of FreeMarker engine,
         * now named "FreeMarker Classic", hence the name).
         * <ul>
         * <li>handle undefined expressions gracefully. Namely when an expression
         *   "expr" evaluates to null:
         *   <ul>
         *     <li>as argument of the <tt>&lt;assign varname=expr></tt> directive, 
         *       <tt>${expr}</tt> directive, <tt>otherexpr == expr</tt> or 
         *       <tt>otherexpr != expr</tt> conditional expressions, or 
         *       <tt>hash[expr]</tt> expression, then it is treated as empty string.
         *     </li>
         *     <li>as argument of <tt>&lt;list expr as item></tt> or 
         *       <tt>&lt;foreach item in expr></tt>, the loop body is not executed
         *       (as if it were a 0-length list)
         *     </li>
         *     <li>as argument of <tt>&lt;if></tt> directive, or otherwise where a
         *       boolean expression is expected, it is treated as false
         *     </li>
         *   </ul>
         * </li>
         * <li>Non-boolean models are accepted in <tt>&lt;if></tt> directive,
         *   or as operands of logical operators. "Empty" models (zero-length string,
         * empty sequence or hash) are evaluated as false, all others are evaluated as
         * true.</li>
         * <li>When boolean value is treated as a string (i.e. output in 
         *   <tt>${...}</tt> directive, or concatenated with other string), true 
         * values are converted to string "true", false values are converted to 
         * empty string.
         * </li>
         * <li>Scalar models supplied to <tt>&lt;list></tt> and 
         *   <tt>&lt;foreach></tt> are treated as a one-element list consisting
         *   of the passed model.
         * </li>
         * <li>Paths parameter of <tt>&lt;include></tt> will be interpreted as
         * absolute path.
         * </li>
         * </ul>
         * In all other aspects, the engine is a 2.1 engine even in compatibility
         * mode - you don't lose any of the new functionality by enabling it.
         */

        public bool IsClassicCompatible()
        {
            return _classicCompatible.HasValue ? _classicCompatible.Value : _parent.IsClassicCompatible();
        }

        /**
         * Sets the locale to assume when searching for template files with no 
         * explicit requested locale.
         */
        public void SetLocale(CultureInfo locale)
        {
            if (locale == null)
            {
                throw new ArgumentException("Setting \"locale\" can't be null");
            }
            _locale = locale;
            _properties[LocaleKey] = locale.Name;
        }

        /**
         * Returns the time zone to use when formatting time values. Defaults to 
         * system time zone.
         */
        public TimeZoneInfo GetTimeZone()
        {
            return _timeZone ?? _parent.GetTimeZone();
        }

        /**
         * Sets the time zone to use when formatting time values. 
         */
        public void SetTimeZone(TimeZoneInfo timeZone)
        {
            if (timeZone == null)
            {
                throw new ArgumentException("Setting \"time_zone\" can't be null");
            }
            _timeZone = timeZone;
            _properties[TimeZoneKey] = timeZone.Id;
        }

        /**
         * Returns the assumed locale when searching for template files with no
         * explicit requested locale. Defaults to system locale.
         */
        public CultureInfo GetLocale()
        {
            return _locale ?? _parent.GetLocale();
        }

        /**
         * Sets the number format used to convert numbers to strings.
         */

        public virtual void SetNumberFormat(string numberFormat)
        {
            if (numberFormat == null) throw new ArgumentException("Setting \"number_format\" can't be null");
            _numberFormat = numberFormat;
            _properties[NumberFormatKey] = numberFormat;
        }

        /**
         * Returns the default number format used to convert numbers to strings.
         * Defaults to <tt>"number"</tt>
         */

        public string GetNumberFormat()
        {
            return _numberFormat ?? _parent.GetNumberFormat();
        }

        public void SetBooleanFormat(string booleanFormat)
        {
            if (booleanFormat == null)
            {
                throw new ArgumentException("Setting \"boolean_format\" can't be null");
            }
            int comma = booleanFormat.IndexOf(Comma);
            if (comma == -1)
            {
                throw new ArgumentException(
                    "Setting \"boolean_format\" must consist of two comma-separated values for true and false respectively");
            }
            _trueFormat = booleanFormat.Substring(0, comma);
            _falseFormat = booleanFormat.Substring(comma + 1);
            _properties[BooleanFormatKey] = booleanFormat;
        }

        public string GetBooleanFormat()
        {
            if (_trueFormat == null)
            {
                return _parent.GetBooleanFormat();
            }
            return _trueFormat + Comma + _falseFormat;
        }

        internal string GetBooleanFormat(bool value)
        {
            return value ? GetTrueFormat() : GetFalseFormat();
        }

        private string GetTrueFormat()
        {
            return _trueFormat ?? _parent.GetTrueFormat();
        }

        private string GetFalseFormat()
        {
            return _falseFormat ?? _parent.GetFalseFormat();
        }

        /**
         * Sets the date format used to convert date models representing time-only
         * values to strings.
         */

        public virtual void SetTimeFormat(string timeFormat)
        {
            if (timeFormat == null) throw new ArgumentException("Setting \"time_format\" can't be null");
            _timeFormat = timeFormat;
            _properties[TimeFormatKey] = timeFormat;
        }

        /**
         * Returns the date format used to convert date models representing
         * time-only dates to strings.
         * Defaults to <tt>"time"</tt>
         */

        public string GetTimeFormat()
        {
            return _timeFormat ?? _parent.GetTimeFormat();
        }

        /**
         * Sets the date format used to convert date models representing date-only
         * dates to strings.
         */

        public virtual void SetDateFormat(string dateFormat)
        {
            if (dateFormat == null) throw new ArgumentException("Setting \"date_format\" can't be null");
            _dateFormat = dateFormat;
            _properties[DateFormatKey] = dateFormat;
        }

        /**
         * Returns the date format used to convert date models representing 
         * date-only dates to strings.
         * Defaults to <tt>"date"</tt>
         */

        public string GetDateFormat()
        {
            return _dateFormat ?? _parent.GetDateFormat();
        }

        /**
         * Sets the date format used to convert date models representing datetime
         * dates to strings.
         */

        public virtual void SetDateTimeFormat(string dateTimeFormat)
        {
            if (dateTimeFormat == null) throw new ArgumentException("Setting \"datetime_format\" can't be null");
            _dateTimeFormat = dateTimeFormat;
            _properties[DatetimeFormatKey] = dateTimeFormat;
        }

        /**
         * Returns the date format used to convert date models representing datetime
         * dates to strings.
         * Defaults to <tt>"datetime"</tt>
         */

        public string GetDateTimeFormat()
        {
            return _dateTimeFormat ?? _parent.GetDateTimeFormat();
        }

        /**
         * Sets the exception handler used to handle template exceptions. 
         *
         * @param templateExceptionHandler the template exception handler to use for 
         * handling {@link TemplateException}s. By default, 
         * {@link TemplateExceptionHandler#HTML_DEBUG_HANDLER} is used.
         */
        public virtual void SetTemplateExceptionHandler(ITemplateExceptionHandler templateExceptionHandler)
        {
            if (templateExceptionHandler == null) throw new ArgumentException("Setting \"template_exception_handler\" can't be null");
            _templateExceptionHandler = templateExceptionHandler;
            _properties[TemplateExceptionHandlerKey] = templateExceptionHandler.GetType().FullName;
        }

        /**
         * Retrieves the exception handler used to handle template exceptions. 
         */
        public ITemplateExceptionHandler GetTemplateExceptionHandler()
        {
            return _templateExceptionHandler ?? _parent.GetTemplateExceptionHandler();
        }

        /**
         * Sets the arithmetic engine used to perform arithmetic operations.
         *
         * @param arithmeticEngine the arithmetic engine used to perform arithmetic
         * operations.By default, {@link ArithmeticEngine#BIGDECIMAL_ENGINE} is 
         * used.
         */
        public void SetArithmeticEngine(ArithmeticEngine arithmeticEngine)
        {
            if (arithmeticEngine == null) throw new ArgumentException("Setting \"arithmetic_engine\" can't be null");
            _arithmeticEngine = arithmeticEngine;
            _properties[ArithmeticEngineKey] = _arithmeticEngine.GetType().FullName;
        }

        /**
         * Retrieves the arithmetic engine used to perform arithmetic operations.
         */
        public ArithmeticEngine GetArithmeticEngine()
        {
            return _arithmeticEngine ?? _parent.GetArithmeticEngine();
        }

        /// <summary>
        /// Sets the object wrapper used to wrap objects to template models.
        /// </summary>
        /// <param name="objectWrapper">
        /// The object wrapper used to wrap objects to template
        /// models. By default, <see cref="ObjectWrapper.DefaultWrapper"/> is used.
        /// </param>
        public void SetObjectWrapper(IObjectWrapper objectWrapper)
        {
            if (objectWrapper == null)
            {
                throw new ArgumentException("Setting \"object_wrapper\" can't be null");
            }
            _objectWrapper = objectWrapper;
            _properties[ObjectWrapperKey] = objectWrapper.GetType().FullName;
        }

        /// <summary>
        /// Retrieves the object wrapper used to wrap objects to template models.
        /// </summary>
        /// <returns>The object wrapper used to wrap objects to template models.</returns>
        public IObjectWrapper GetObjectWrapper()
        {
            return _objectWrapper ?? _parent.GetObjectWrapper();
        }

        /// <summary>
        /// Sets the output encoding. Allows <c>null</c>, which means that the
        /// output encoding is not known.
        /// </summary>
        /// <param name="outputEncoding">The new output encoding.</param>
        public virtual void SetOutputEncoding(Encoding outputEncoding)
        {
            // NOTE: Does Dictionary allow null value?

            _outputEncoding = outputEncoding;
            // java.util.Properties doesn't allow null value!
            if (outputEncoding != null)
            {
                _properties[OutputEncodingKey] = outputEncoding.WebName;
            }
            else
            {
                _properties.Remove(OutputEncodingKey);
            }
            _outputEncodingSet = true;
        }

        public Encoding GetOutputEncoding()
        {
            return _outputEncodingSet
                       ? _outputEncoding
                       : (_parent != null ? _parent.GetOutputEncoding() : null);
        }

        /**
         * Sets the URL escaping charset. Allows <code>null</code>, which means that the
         * output encoding will be used for URL escaping.
         */

        public virtual void SetUrlEscapingCharset(Encoding urlEscapingCharset)
        {
            // NOTE: Does Dictionary allow null value?

            _urlEscapingCharset = urlEscapingCharset;
            // java.util.Properties doesn't allow null value!
            if (urlEscapingCharset != null)
            {
                _properties[UrlEscapingCharsetKey] = urlEscapingCharset.WebName;
            }
            else
            {
                _properties.Remove(UrlEscapingCharsetKey);
            }
            _urlEscapingCharsetSet = true;
        }

        public Encoding GetUrlEscapingCharset()
        {
            return _urlEscapingCharsetSet
                       ? _urlEscapingCharset
                       : (_parent != null ? _parent.GetUrlEscapingCharset() : null);
        }

        /**
         * Sets a setting by a name and string value.
         * 
         * <p>List of supported names and their valid values:
         * <ul>
         *   <li><code>"locale"</code>: local codes with the usual format, such as <code>"en_US"</code>.
         *   <li><code>"classic_compatible"</code>:
         *       <code>"true"</code>, <code>"false"</code>, <code>"yes"</code>, <code>"no"</code>,
         *       <code>"t"</code>, <code>"f"</code>, <code>"y"</code>, <code>"n"</code>.
         *       Case insensitive.
         *   <li><code>"template_exception_handler"</code>:  If the value contains dot, then it is
         *       interpreted as class name, and the object will be created with
         *       its parameterless constructor. If the value does not contain dot,
         *       then it must be one of these special values:
         *       <code>"rethrow"</code>, <code>"debug"</code>,
         *       <code>"html_debug"</code>, <code>"ignore"</code> (case insensitive).
         *   <li><code>"arithmetic_engine"</code>: If the value contains dot, then it is
         *       interpreted as class name, and the object will be created with
         *       its parameterless constructor. If the value does not contain dot,
         *       then it must be one of these special values:
         *       <code>"bigdecimal"</code>, <code>"conservative"</code> (case insensitive).  
         *   <li><code>"object_wrapper"</code>: If the value contains dot, then it is
         *       interpreted as class name, and the object will be created with
         *       its parameterless constructor. If the value does not contain dot,
         *       then it must be one of these special values:
         *       <code>"simple"</code>, <code>"beans"</code>, <code>"jython"</code> (case insensitive).
         *   <li><code>"number_format"</code>: pattern as <code>java.text.DecimalFormat</code> defines.
         *   <li><code>"boolean_format"</code>: the textual value for boolean true and false,
         *       separated with comma. For example <code>"yes,no"</code>.
         *   <li><code>"date_format", "time_format", "datetime_format"</code>: patterns as
         *       <code>java.text.SimpleDateFormat</code> defines.
         *   <li><code>"time_zone"</code>: time zone, with the format as
         *       <code>java.util.TimeZone.getTimeZone</code> defines. For example <code>"GMT-8:00"</code> or
         *       <code>"America/Los_Angeles"</code>
         *   <li><code>"output_encoding"</code>: Informs FreeMarker about the charset
         *       used for the output. As FreeMarker outputs character stream (not
         *       byte stream), it is not aware of the output charset unless the
         *       software that encloses it tells it explicitly with this setting.
         *       Some templates may use FreeMarker features that require this.</code>
         *   <li><code>"url_escaping_charset"</code>: If this setting is set, then it
         *       overrides the value of the <code>"output_encoding"</code> setting when
         *       FreeMarker does URL encoding.
         * </ul>
         * 
         * @param key the name of the setting.
         * @param value the string that describes the new value of the setting.
         * 
         * @throws UnknownSettingException if the key is wrong.
         * @throws TemplateException if the new value of the setting can't be set
         *     for any other reasons.
         */

        public void SetSetting(string key, string value)
        {
            try
            {
                if (LocaleKey == key)
                {
                    SetLocale(StringUtil.DeduceLocale(value));
                }
                else if (NumberFormatKey == key)
                {
                    SetNumberFormat(value);
                }
                else if (TimeFormatKey == key)
                {
                    SetTimeFormat(value);
                }
                else if (DateFormatKey == key)
                {
                    SetDateFormat(value);
                }
                else if (DatetimeFormatKey == key)
                {
                    SetDateTimeFormat(value);
                }
                else if (TimeZoneKey == key)
                {
                    SetTimeZone(TimeZoneInfo.FindSystemTimeZoneById(value));
                }
                else if (ClassicCompatibleKey == key)
                {
                    SetClassicCompatible(StringUtil.GetYesNo(value));
                }
                else if (TemplateExceptionHandlerKey == key)
                {
                    if (value.IndexOf('.') == -1)
                    {
                        if ("debug".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            SetTemplateExceptionHandler(TemplateExceptionHandler.DebugHandler);
                        }
                        else if ("html_debug".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            SetTemplateExceptionHandler(TemplateExceptionHandler.HtmlDebugHandler);
                        }
                        else if ("ignore".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            SetTemplateExceptionHandler(TemplateExceptionHandler.IgnoreHandler);
                        }
                        else if ("rethrow".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            SetTemplateExceptionHandler(TemplateExceptionHandler.RethrowHandler);
                        }
                        else
                        {
                            throw InvalidSettingValueException(key, value);
                        }
                    }
                    else
                    {
                        SetTemplateExceptionHandler(
                            (ITemplateExceptionHandler) Activator.CreateInstance(
                                Type.GetType(value)));
                    }
                }
                else if (ArithmeticEngineKey == key)
                {
                    if (value.IndexOf('.') == -1)
                    {
                        if ("bigdecimal".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            //SetArithmeticEngine(ArithmeticEngine.BigDecimalEngine);
                            throw new NotImplementedException();
                        }
                        else if ("conservative".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            SetArithmeticEngine(ArithmeticEngine.ConservativeEngine);
                        }
                        else
                        {
                            throw InvalidSettingValueException(key, value);
                        }
                    }
                    else
                    {
                        SetArithmeticEngine(
                            (ArithmeticEngine) Activator.CreateInstance(
                                Type.GetType(value)));
                    }
                }
                else if (ObjectWrapperKey == key)
                {
                    if (value.IndexOf('.') == -1)
                    {
                        if ("default".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            SetObjectWrapper(ObjectWrapper.DefaultWrapper);
                        }
                        else if ("simple".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            SetObjectWrapper(ObjectWrapper.SimpleWrapper);
                        }
                        else if ("beans".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            SetObjectWrapper(ObjectWrapper.BeansWrapper);
                        }
                        else if ("jython".Equals(value, StringComparison.OrdinalIgnoreCase))
                        {
                            //Class clazz = Class.forName(
                            //        "freemarker.ext.jython.JythonWrapper");
                            //setObjectWrapper(
                            //        (ObjectWrapper) clazz.getField("INSTANCE").get(null));
                            throw new NotImplementedException();
                        }
                        else
                        {
                            throw InvalidSettingValueException(key, value);
                        }
                    }
                    else
                    {
                        SetObjectWrapper(
                            (IObjectWrapper) Activator.CreateInstance(
                                Type.GetType(value)));
                    }
                }
                else if (BooleanFormatKey == key)
                {
                    SetBooleanFormat(value);
                }
                else if (OutputEncodingKey == key)
                {
                    SetOutputEncoding(Encoding.GetEncoding(value));
                }
                else if (UrlEscapingCharsetKey == key)
                {
                    SetUrlEscapingCharset(Encoding.GetEncoding(value));
                }
                else if (StrictBeanModels == key)
                {
                    SetStrictBeanModels(StringUtil.GetYesNo(value));
                }
                else
                {
                    throw CreateUnknownSettingException(key);
                }
            }
            catch (TemplateException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new TemplateException(
                    "Failed to set setting " + key + " to value " + value,
                    e, GetEnvironment());
            }
        }

        public void SetStrictBeanModels(bool strict)
        {
            if (!(_objectWrapper is BeansWrapper))
            {
                throw new InvalidOperationException("Not a beans wrapper");
            }
            ((BeansWrapper) _objectWrapper).SetStrict(strict);
        }

        /**
         * Returns the textual representation of a setting.
         * @param key the setting key. Can be any of standard <tt>XXX_KEY</tt>
         * constants, or a custom key.
         *
         * @deprecated This method was always defective, and certainly it always
         *     will be. Don't use it. (Simply, it's hardly possible in general to
         *     convert setting values to text in a way that ensures that
         *     {@link #setSetting(string, string)} will work with them correctly.)
         */

        public string GetSetting(string key)
        {
            return _properties[key];
        }

        //    /**
        //     * This meant to return the string-to-string <code>Map</code> of the
        //     * settings. So it actually should return a <code>Properties</code> object,
        //     * but it doesn't by mistake. The returned <code>Map</code> is read-only,
        //     * but it will reflect the further configuration changes (aliasing effect).
        //     *
        //     * @deprecated This method was always defective, and certainly it always
        //     *     will be. Don't use it. (Simply, it's hardly possible in general to
        //     *     convert setting values to text in a way that ensures that
        //     *     {@link #setSettings(Properties)} will work with them correctly.)
        //     */
        //    public Map getSettings() {
        //        return Collections.unmodifiableMap(properties);
        //    }

        protected Environment GetEnvironment()
        {
            return this is Environment
                       ? (Environment) this
                       : Environment.GetCurrentEnvironment();
        }

        protected TemplateException CreateUnknownSettingException(string name)
        {
            return new UnknownSettingException(name, GetEnvironment());
        }

        protected TemplateException InvalidSettingValueException(string name, string value)
        {
            return new TemplateException("Invalid value for setting " + name + ": " + value, GetEnvironment());
        }

        public class UnknownSettingException : TemplateException
        {
            internal UnknownSettingException(string name, Environment env)
                : base("Unknown setting: " + name, env)
            {
            }
        }

        /**
         * Set the settings stored in a <code>Properties</code> object.
         * 
         * @throws TemplateException if the <code>Properties</code> object contains
         *     invalid keys, or invalid setting values, or any other error occurs
         *     while changing the settings.
         */

        public void SetSettings(Dictionary<string, string> props)
        {
            foreach (KeyValuePair<string, string> property in props)
            {
                SetSetting(property.Key, property.Value);
            }
        }

        //    /**
        //     * Reads a setting list (key and element pairs) from the input stream.
        //     * The stream has to follow the usual <code>.properties</code> format.
        //     *
        //     * @throws TemplateException if the stream contains
        //     *     invalid keys, or invalid setting values, or any other error occurs
        //     *     while changing the settings.
        //     * @throws IOException if an error occurred when reading from the input stream.
        //     */
        //    public void setSettings(InputStream propsIn) throws TemplateException, IOException {
        //        Properties p = new Properties();
        //        p.load(propsIn);
        //        setSettings(p);
        //    }

        /**
         * Internal entry point for setting unnamed custom attributes
         */
        internal void SetCustomAttribute(object key, object value)
        {
            lock (_customAttributes)
            {
                _customAttributes.Add(key, value);
            }
        }

        /**
         * Internal entry point for getting unnamed custom attributes
         */
        internal object GetCustomAttribute(object key, CustomAttribute attr)
        {
            lock (_customAttributes)
            {
                object o = _customAttributes[key];
                if (o == null && !_customAttributes.ContainsKey(key))
                {
                    o = attr.Create();
                    _customAttributes.Add(key, o);
                }
                return o;
            }
        }

        /**
         * Sets a named custom attribute for this configurable.
         *
         * @param name the name of the custom attribute
         * @param value the value of the custom attribute. You can set the value to
         * null, however note that there is a semantic difference between an
         * attribute set to null and an attribute that is not present, see
         * {@link #removeCustomAttribute(string)}.
         */
        public void SetCustomAttribute(string name, object value)
        {
            lock (_customAttributes)
            {
                _customAttributes[name] = value;
            }
        }

        /**
         * Returns an array with names of all custom attributes defined directly 
         * on this configurable. (That is, it doesn't contain the names of custom attributes
         * defined indirectly on its parent configurables.) The returned array is never null,
         * but can be zero-length.
         * The order of elements in the returned array is not defined and can change
         * between invocations.  
         */
        public string[] GetCustomAttributeNames()
        {
                lock (_customAttributes)
                {
                    return _customAttributes
                        .Keys
                        .OfType<string>()
                        .ToArray();
                }
            }

        /**
         * Removes a named custom attribute for this configurable. Note that this
         * is different than setting the custom attribute value to null. If you
         * set the value to null, {@link #getCustomAttribute(string)} will return
         * null, while if you remove the attribute, it will return the value of
         * the attribute in the parent configurable (if there is a parent 
         * configurable, that is). 
         *
         * @param name the name of the custom attribute
         */

        public void RemoveCustomAttribute(string name)
        {
            lock (_customAttributes)
            {
                _customAttributes.Remove(name);
            }
        }

        /**
         * Retrieves a named custom attribute for this configurable. If the 
         * attribute is not present in the configurable, and the configurable has
         * a parent, then the parent is looked up as well.
         *
         * @param name the name of the custom attribute
         *
         * @return the value of the custom attribute. Note that if the custom attribute
         * was created with <tt>&lt;#ftl&nbsp;attributes={...}></tt>, then this value is already
         * unwrapped (i.e. it's a <code>string</code>, or a <code>List</code>, or a
         * <code>Map</code>, ...etc., not a FreeMarker specific class).
         */
        public object GetCustomAttribute(string name)
        {
            object retval;
            lock (_customAttributes)
            {
                retval = _customAttributes[name];
                if (retval == null && _customAttributes.ContainsKey(name))
                {
                    return null;
                }
            }
            if (retval == null && _parent != null)
            {
                return _parent.GetCustomAttribute(name);
            }
            return retval;
        }

        protected void DoAutoImportsAndIncludes(Environment env)
        {
            if (_parent != null)
            {
                _parent.DoAutoImportsAndIncludes(env);
            }
        }
    }
}